<?php

namespace App\Models\Payment\Platform;

use App\Jobs\PaymentOrderPaid;
use App\Models\Common\Constant;
use GuzzleHttp\Client;
use Illuminate\Support\Arr;
use App\Models\Payment\TradeType;
use Illuminate\Support\Facades\Log;
use App\Models\Payment\PaymentOrder;
use App\Models\Payment\PaymentChannel;

/**
 * 银联商务
 * https://www.chinaums.com
 */
class ChinaUms extends BasePlatform implements BasePlatformInterface
{
    const RULE = [
        'mid' => 'required|string',
        'tid' => 'required|string',
        'msgSrc' => 'nullable|string',
        'msgSrcId' => 'required|string',
        'key' => 'required|string',
        'appId' => 'required|string',
        'appKey' => 'required|string',
        'mini_appId' => 'nullable|string'
    ];

    const NOTIFY_CODE_SUCCESS = 'SUCCESS';
    const NOTIFY_CODE_FAIL = 'FAIL';

    protected $version;
    protected $keyFile;
    protected $certFile;
    protected $mid;
    protected $tid;
    protected $msgSrc;
    protected $msgSrcId;
    protected $key;
    protected $appId;
    protected $appKey;
    protected $miniAppId;
    protected $headerAuthorization;

    const INST_MID_MINI = 'MINIDEFAULT';
    const INST_MID_APP = 'APPDEFAULT';
    const INST_MID_QRPAY = 'QRPAYDEFAULT';
    const INST_MID_YUEDAN = 'YUEDANDEFAULT';
    const INST_MID_H5 = 'H5DEFAULT';

    const GATEWAY = 'https://api-mop.chinaums.com/%s/netpay/';
    const GATEWAY_DEV = 'https://test-api-open.chinaums.com/%s/netpay/';

    const TRADE_TYPE_METHOD_GET = [TradeType::ALI_MWEB, TradeType::WECHAT_MWEB, TradeType::WECHAT_JSAPI_MWEB, TradeType::ALI_NATIVE, TradeType::WECHAT_NATIVE];

    public function __construct(PaymentChannel $channel, bool $sandbox = false, string $version = 'v1')
    {
        $this->version = $version;
        $this->certFile = $channel->cert_file;
        $this->keyFile = $channel->key_file;
        $parameters = $channel->parameters ?: [];
        $this->mid = Arr::get($parameters, 'mid');
        $this->tid = Arr::get($parameters, 'tid');
        $this->msgSrc = Arr::get($parameters, 'msgSrc');
        $this->msgSrcId = Arr::get($parameters, 'msgSrcId');
        $this->key = Arr::get($parameters, 'key');
        $this->appId = Arr::get($parameters, 'appId');
        $this->appKey = Arr::get($parameters, 'appKey');
        $this->miniAppId = Arr::get($parameters, 'mini_appId');
        $this->gateway = $sandbox ? sprintf(self::GATEWAY_DEV, $this->version) : sprintf(self::GATEWAY, $this->version);

        $this->logger = Log::channel('payment-chinaums');
    }

    public static function nonceStr(): string
    {
        return md5(uniqid(microtime(true), true));
    }

    public static function timestamp(): string
    {
        return now()->format('YmdHis');
    }

    protected function signature(): string
    {
        $appId = $this->appId;
        $appKey = $this->appKey;
        $timestamp = $this->timestamp;
        $nonceStr = $this->nonceStr;
        $toBeEncryptedStr = bin2hex(hash('sha256', json_encode($this->body), true));
        return base64_encode(hash_hmac('sha256', "$appId$timestamp$nonceStr$toBeEncryptedStr", $appKey, true));
    }

    public function paidCode(): string
    {
        return retry(3, function () {
            $paidCode = $this->msgSrcId . $this->timestamp() . str_pad(mt_rand(0, 999999), 6, "0", STR_PAD_BOTH);
            if (!PaymentOrder::where('paid_code', $paidCode)->exists()) {
                return $paidCode;
            }
            throw new \Exception('NULL PAID CODE');
        }, 0.1);
    }

    public function fire()
    {
        $httpClient = new Client(['base_uri' => $this->gateway, 'timeout' => self::TIMEOUT, 'verify' => false]);
        $httpMethod = strtoupper($this->method);
        $respBody = [];

        if ($httpMethod === 'POST') {
            $this->body = ['json' => $this->body, 'headers' => ['Authorization' => $this->headerAuthorization]];
        } else {
            $this->body = ['body' => $this->body];
            $httpMethod = 'GET';
        }

        $logPacket = ['gateway' => $this->gateway, 'uri' => $this->uri, 'authorization' => $this->headerAuthorization, 'queryBody' => $this->body];

        try {
            $respObj = $httpClient->request($httpMethod, $this->uri, $this->body);
            $respBodyContents = $respObj->getBody()->getContents();
            $respBody = @json_decode($respBodyContents, true) ?: [];
            $logPacket['response'] = $respBody ?: $respBodyContents;
        } catch (\Throwable $exception) {
            $logPacket['err'] = $exception->getMessage();
        } finally {

            $this->logger->info($this->name, $logPacket);

            if (isset($exception)) {
                throw $exception;
            }
        }

        $this->responseAdapter($respBody);

        return $respBody;
    }

    static public function optionalDatValidator(string $tradeType, array $optionalDat): array
    {
        return [];
    }

    public function toPay(string $tradeType, PaymentOrder $paymentOrder)
    {
        if ($paymentOrder->paid_at || $paymentOrder->order_status === Constant::ORDER_STATUS_PAID) {
            throw new \Exception($paymentOrder->paid_code . '已完成支付的订单不可重复发起支付');
        }

        $this->name = '申请支付参数:' . $tradeType;
        $this->timestamp = self::timestamp();
        $this->nonceStr = self::nonceStr();

        $queryBody = [
            'requestTimestamp' => $this->timestamp,
            'merOrderId' => $paymentOrder->paid_code,
            'mid' => $this->mid,
            'tid' => $this->tid,
            'orderDesc' => $paymentOrder->order_title,
            'totalAmount' => $paymentOrder->amount,
            'expireTime' => now()->addMinutes(10)->format('Y-m-d H:i:s'),
            'notifyUrl' => $this->notifyUrl($paymentOrder->pay_type, $paymentOrder->payment_channel_id)
        ];

        switch ($tradeType) {
            case TradeType::ALI_JSAPI:
                $queryBody['instMid'] = self::INST_MID_YUEDAN;
                $queryBody['userId'] = $paymentOrder->payer_id;
                $queryBody['tradeType'] = 'JSAPI';
                $this->uri = 'trade/create';
                $this->method = 'post';
                break;
            case TradeType::WECHAT_JSAPI:
                $queryBody['instMid'] = self::INST_MID_YUEDAN;
                $queryBody['tradeType'] = 'JSAPI';
                $queryBody['subOpenId'] = $paymentOrder->payer_id;
                $this->uri = 'wx/unified-order';
                $this->method = 'post';
                if (!$paymentOrder->payer_id) {
                    throw new \Exception('openid为空');
                }
                break;
            case TradeType::WECHAT_JSAPI_MWEB:
                $queryBody['instMid'] = self::INST_MID_YUEDAN;
                $queryBody['subOpenId'] = $paymentOrder->payer_id;
                $this->uri = 'webpay/pay';
                $this->method = 'get';
                break;
            case TradeType::WECHAT_MWEB:
            case TradeType::WECHAT_NATIVE:
                $queryBody['instMid'] = self::INST_MID_H5;
                $this->uri = 'wxpay/h5-pay';
                $this->method = 'get';
                break;
            case TradeType::ALI_MWEB:
            case TradeType::ALI_NATIVE:
                $queryBody['instMid'] = self::INST_MID_H5;
                $this->uri = 'trade/h5-pay';
                $this->method = 'get';
                break;
            case TradeType::WECHAT_MINI:
                if (!$this->miniAppId) {
                    throw new \Exception('支付方式为(' . TradeType::WECHAT_MINI . ')时未配置mini_appId');
                }
                $this->uri = 'wx/unified-order';
                $queryBody['instMid'] = self::INST_MID_MINI;
                $queryBody['subAppId'] = $this->miniAppId;
                $queryBody['subOpenId'] = $paymentOrder->payer_id;
                $queryBody['tradeType'] = 'MINI';
                $this->method = 'post';
                if (!$paymentOrder->payer_id) {
                    throw new \Exception('openid为空');
                }
                break;
            default:
                throw new \Exception('不支持的支付方式');
        }

        $queryBody = array_filter($queryBody);

        if ($this->method === 'post') {
            $this->body = $queryBody;
            $signature = $this->signature();
            $this->headerAuthorization = "OPEN-BODY-SIG AppId=\"$this->appId\", Timestamp=\"$this->timestamp\", Nonce=\"$this->nonceStr\", Signature=\"$signature\"";
        } else {
            $this->body = [
                'authorization' => 'OPEN-FORM-PARAM',
                'appId' => $this->appId,
                'timestamp' => $this->timestamp,
                'nonce' => $this->nonceStr,
                'content' => json_encode($queryBody),
                'signature' => $this->signature()
            ];
        }

        if (in_array($tradeType, self::TRADE_TYPE_METHOD_GET, true)) {
            $this->body['content'] = urlencode($this->body['content']);
            $this->body['signature'] = urlencode($this->body['signature']);
            $payResponse = ['errCode' => 'SUCCESS', 'mweb_url' => $this->gateway . $this->uri . '?' . joinStr($this->body)];
            $this->responseAdapter($payResponse);
        } else {
            $payResponse = $this->fire();
        }

        $errMsg = '';
        if (Arr::get($payResponse, 'errCode') === 'SUCCESS') {
            switch ($tradeType) {
                case TradeType::ALI_JSAPI:
                case TradeType::WECHAT_JSAPI:
                    $payResponse['pay_info'] = Arr::get($payResponse, 'jsPayRequest') ?: [];
                    unset($payResponse['jsPayRequest']);
                    break;
                case TradeType::WECHAT_MINI:
                    $payResponse['pay_info'] = Arr::get($payResponse, 'miniPayRequest') ?: [];
                    unset($payResponse['miniPayRequest']);
                case TradeType::ALI_MWEB:
                case TradeType::WECHAT_MWEB:
                case TradeType::WECHAT_JSAPI_MWEB:
                case TradeType::WECHAT_NATIVE:
                case TradeType::ALI_NATIVE:
                    $payResponse['pay_info']['qr_code'] = Arr::get($payResponse, 'mweb_url') ?: '';
                    break;
                default:
                    break;
            }
        } else {
            $errMsg = strval(Arr::get($payResponse, 'return_msg')) ?: '未知原因';
        }
        $paymentOrder->error = $errMsg;
        $paymentOrder->save();

        return $payResponse;
    }

    public function toQuery(PaymentOrder $paymentOrder)
    {
        $this->name = '订单查询';
        $this->method = 'post';
        $this->uri = 'query';
        $this->timestamp = self::timestamp();
        $this->nonceStr = self::nonceStr();

        $this->body = [
            'requestTimestamp' => $this->timestamp,
            'merOrderId' => $paymentOrder->paid_code,
            'mid' => $this->mid,
            'tid' => $this->tid,
            'instMid' => self::INST_MID_YUEDAN
        ];

        $signature = $this->signature();
        $this->headerAuthorization = "OPEN-BODY-SIG AppId=\"$this->appId\", Timestamp=\"$this->timestamp\", Nonce=\"$this->nonceStr\", Signature=\"$signature\"";
        return $this->fire();
    }

    public function toRefund($refundOrder)
    {
        $this->name = '订单退款';
        $this->method = 'post';
        $this->uri = 'refund';
        $this->timestamp = self::timestamp();
        $this->nonceStr = self::nonceStr();

        $this->body = [
            'requestTimestamp' => $this->timestamp,
            'merOrderId' => $refundOrder->paid_code,
            'mid' => $this->mid,
            'tid' => $this->tid,
            'instMid' => self::INST_MID_YUEDAN,
            'refundAmount' => $refundOrder->refund_amount,
            'refundOrderId' => $refundOrder->refund_code
        ];

        $signature = $this->signature();
        $this->headerAuthorization = "OPEN-BODY-SIG AppId=\"$this->appId\", Timestamp=\"$this->timestamp\", Nonce=\"$this->nonceStr\", Signature=\"$signature\"";
        return $this->fire();
    }

    public function toClose(PaymentOrder $paymentOrder)
    {
        $this->name = '订单关闭';
        $this->method = 'post';
        $this->uri = 'close';
        $this->timestamp = self::timestamp();
        $this->nonceStr = self::nonceStr();

        $this->body = [
            'requestTimestamp' => $this->timestamp,
            'merOrderId' => $paymentOrder->paid_code,
            'mid' => $this->mid,
            'tid' => $this->tid,
            'instMid' => self::INST_MID_YUEDAN
        ];

        $signature = $this->signature();
        $this->headerAuthorization = "OPEN-BODY-SIG AppId=\"$this->appId\", Timestamp=\"$this->timestamp\", Nonce=\"$this->nonceStr\", Signature=\"$signature\"";
        return $this->fire();
    }

    public function handleNotify(PaymentOrder $paymentOrder, array $notifyDat)
    {
        $logName = $paymentOrder->pay_type . ':' . $paymentOrder->payment_channel_id;

        $paymentSuccessful = false;

        if (!$this->notifySignCheck($notifyDat)) {
            Log::channel('payment-notify-error')->info($logName, ['err' => '支付回调验签失败', 'notifyDat' => $notifyDat, 'paid_code' => $paymentOrder->paid_code]);
            return self::NOTIFY_CODE_FAIL;
        }

        if (!$seqID = Arr::get($notifyDat, 'seqId')) {
            Log::channel('payment-notify-error')->info($logName, ['err' => '支付回调缺失重要参数流水单号', 'notifyDat' => $notifyDat, 'paid_code' => $paymentOrder->paid_code]);
            return self::NOTIFY_CODE_FAIL;
        }

        if ($paymentOrder->order_status === Constant::ORDER_STATUS_PAID) {
            if ($paymentOrder->out_trade_no && ($paymentOrder->out_trade_no !== $seqID)) {
                Log::channel('payment-notify-error')->info($logName, ['err' => '订单重复支付(seqID)', 'notifyDat' => $notifyDat, 'paid_code' => $paymentOrder->paid_code]);
                return self::NOTIFY_CODE_SUCCESS;
            } else {
                Log::channel('payment-notify-error')->info($logName, ['err' => '订单重复支付', 'notifyDat' => $notifyDat, 'paid_code' => $paymentOrder->paid_code]);
            }
            return self::NOTIFY_CODE_SUCCESS;
        }


        //对比支付金额

        try {

            if (strtoupper(Arr::get($notifyDat, 'status') === 'TRADE_SUCCESS')) {

                $paymentOrder->paid_at = now()->toDateTimeString();
                $paymentOrder->out_trade_no = $seqID;
                $paymentOrder->frozen_balance_amount = 0;
                $paymentOrder->frozen_point = 0;
                $paymentOrder->order_status = Constant::ORDER_STATUS_PAID;

                $paymentOrder->save();
                $paymentSuccessful = true;
            }
        } catch (\Throwable $exception) {
            Log::channel('payment-notify-error')->info($logName, ['err' => $exception->getMessage(), 'notifyDat' => $notifyDat, 'paid_code' => $paymentOrder->paid_code]);
        } finally {

            if ($paymentSuccessful) {
                //处理后续的各类系统订单逻辑
                PaymentOrderPaid::dispatch($paymentOrder)->onQueue(Constant::QUEUE_PAYMENT);

                return self::NOTIFY_CODE_SUCCESS;
            }
        }
        return self::NOTIFY_CODE_FAIL;
    }

    public function responseAdapter(array &$response = []): array
    {

        if (Arr::get($response, 'errCode') === 'SUCCESS') {
            $response['result_code'] = 'SUCCESS';
            $response['return_code'] = 'SUCCESS';
            $response['return_msg'] = 'OK';

            $response['trade_state_desc'] = $response['trade_state'] = strval(Arr::get($response, 'status'));
        } else {
            $response['result_code'] = 'FAIL';
            $response['return_msg'] = Arr::get($response, 'errMsg') ?: 'ERROR';
            $response['return_code'] = 'FAIL';

            $response['trade_state'] = strval(Arr::get($response, 'errCode'));
            $response['trade_state_desc'] = strval(Arr::get($response, 'errMsg'));
        }
        return $response;
    }

    public static function notifyDatFilter($rawNotifyDat): array
    {
        if (is_string($rawNotifyDat)) {
            parse_str($rawNotifyDat, $notifyDat);
        } else {
            $notifyDat = $rawNotifyDat;
        }

        ksort($notifyDat);
        return array_map(function ($val) {
            if (is_string($val)) {
                $val = urldecode($val);
            }
            return $val;
        }, $notifyDat);
    }

    protected function notifySignCheck($rawNotifyDat): bool
    {
        $str4Sign = joinStr($rawNotifyDat);
        $str4Sign .= $this->key;

        if (Arr::get($rawNotifyDat, 'signType') == 'MD5') {
            $localSign = strtoupper(md5($str4Sign));
        } else {
            $localSign = strtoupper(bin2hex(hash('sha256', $str4Sign, true)));
        }

        if (!$notifySign = Arr::get($rawNotifyDat, 'sign')) {
            return false;
        }

        return $notifySign === $localSign;
    }
}
